package com.diy.sigmund.diygateway.filter;

import com.diy.sigmund.diycommon.entity.ResultMsg;
import com.diy.sigmund.diycommon.entity.constant.CommonConstant;
import com.diy.sigmund.diycommon.entity.constant.GatewayConstant;
import com.diy.sigmund.diycommon.entity.constant.HeaderConstant;
import com.diy.sigmund.diycommon.entity.constant.LogConstant;
import com.diy.sigmund.diycommon.entity.constant.UserConstant;
import com.diy.sigmund.diycommon.entity.enumerate.ResponseEnum;
import com.diy.sigmund.diycommon.middleware.cache.redis.RedisService;
import com.diy.sigmund.diycommon.util.NetworkUtil;
import com.diy.sigmund.diycommon.util.json.JacksonUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import java.io.ByteArrayInputStream;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

/**
 * 鉴权过滤器
 * https://blog.csdn.net/weixin_39823527/article/details/84580229
 * https://blog.csdn.net/liuchuanhong1/article/details/62236793
 * https://blog.csdn.net/wo18237095579/article/details/83543592
 *
 * @author ylm-sigmund
 * @since 2021/9/1 20:20
 */
@Component
public class CustomAccessFilter extends ZuulFilter {

    @Value("${zuul.prefix}")
    private String zuulPrefix;
    @Autowired
    private RedisService redisService;
    private static final Logger log = LoggerFactory.getLogger(CustomAccessFilter.class);

    /**
     * <br>返回一个字符串代表过滤器的类型，在zuul中定义了四种不同生命周期的过滤器类型，具体如下：
     * <br>pre：可以在请求被路由之前调用，在 Zuul 按照规则路由到下级服务之前执行。如果需要对请求进行预处理，比如鉴权、限流等，都应考虑在此类 Filter 实现。
     * <br>route：在路由请求时候被调用，这类 Filter 是 Zuul 路由动作的执行者，是 Apache Http Client 或 Netflix Ribbon 构建和发送原始 HTTP 请求的地方，目前已支持 Okhttp。
     * <br>post：在route和error过滤器之后被调用，这类 Filter 是在源服务返回结果或者异常信息发生后执行的，如果需要对返回信息做一些处理，则在此类 Filter 进行处理。
     * <br>error：处理请求时发生错误时被调用，在整个生命周期内如果发生异常，则会进入 error Filter，可做全局异常处理。
     *
     * @return String
     */
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    /**
     * 通过int值来定义过滤器的执行顺序
     *
     * @return int
     */
    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER + 1;
    }

    /**
     * 返回一个boolean类型来判断该过滤器是否要执行，所以通过此函数可实现过滤器的开关。
     *
     * @return boolean
     */
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        // 前一个过滤器中手动定义false则不执行该过滤器，不定义则会执行
        return !Objects.equals(false, ctx.get(GatewayConstant.PRE_SHOULD_FILTER));
    }

    /**
     * 过滤器的具体逻辑
     * Zuul 内部提供了一个动态读取、编译和运行这些 Filter 的机制。
     * Filter 之间不直接通信，在请求线程中会通过 RequestContext 来共享状态，它的内部是用 ThreadLocal 实现的，
     * 当然你也可以在 Filter之间使用 ThreadLocal 来收集自己需要的状态或数据。
     *
     * @return Object
     * @throws ZuulException ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        if (Objects.isNull(requestContext.get(GatewayConstant.PRE_START_TIME))) {
            requestContext.set(GatewayConstant.PRE_START_TIME, LocalDateTime.now());
        }

        HttpServletRequest request = requestContext.getRequest();
        log.info("URI       : " + request.getMethod() + " " + request.getProtocol() + " " + request.getRequestURI());
        log.info("请求ip是 " + NetworkUtil.getIpAddress(request));
        try {
            return handle(requestContext, request);
        } catch (Exception e) {
            log.error("handle error", e);
            // {"code":"92","msg":"网关服务处理异常，请稍后尝试"}
            setSendZuulResponseFalse(requestContext, ResponseEnum.GATEWAY_FAIL);
        }
        return null;
    }

    /**
     * 令zuul过滤该请求，不对它进行路由
     */
    private void setSendZuulResponseFalse(RequestContext requestContext, ResponseEnum responseEnum) {
        requestContext.setSendZuulResponse(false);
        requestContext.setResponseStatusCode(HttpStatus.OK.value());
        requestContext.setResponseDataStream(
                new ByteArrayInputStream(JacksonUtil.toJson(ResultMsg.fail(responseEnum)).getBytes()));
        requestContext.getResponse().setContentType(HeaderConstant.APPLICATION_JSON_VALUE);
    }

    private Object handle(RequestContext requestContext, HttpServletRequest request) {
        // 校验token，token为空需要校验白名单
        String accessToken = request.getHeader("token");
        if (StringUtils.isBlank(accessToken)) {
            log.warn("token is empty");
            // 白名单中不存在，就直接结束
            if (!this.whitelist().contains(request.getRequestURI().replaceFirst(zuulPrefix, ""))) {
                // {"code":"91","msg":"非法请求"}
                setSendZuulResponseFalse(requestContext, ResponseEnum.ILLEGAL_REQUEST);
                log.warn(LogConstant.SPRING_LOG, "请求网关，非法请求，白名单校验失败，请检查白名单配置");
                // pre类型的后一个过滤器不执行
                requestContext.set(GatewayConstant.PRE_SHOULD_FILTER, false);
                return null;
            }
            log.info(LogConstant.SPRING_LOG, "请求网关，白名单校验成功");
            return null;
        }

        // 当token不为空时，需要刷新token的过期时间
        String key = UserConstant.USER_ACCESS_TOKEN + accessToken;
        log.info("缓存中token对应的key是 {}", key);
        if (!redisService.exists(key)) {
            requestContext.set(GatewayConstant.PRE_SHOULD_FILTER, false);
            setSendZuulResponseFalse(requestContext, ResponseEnum.SESSION_INVALIDATION);
            log.warn(LogConstant.SPRING_LOG, "请求网关，鉴权失败，缓存中该key不存在");
            return null;
        }

        redisService.expire(key, CommonConstant.TOKEN_EXPIRE_TIME, TimeUnit.MINUTES);
        log.info(LogConstant.SPRING_LOG, "缓存中该key存在，刷新token的过期时间");

        log.info(LogConstant.SPRING_LOG, "请求网关，鉴权成功");
        return null;
    }

    /**
     * 白名单
     */
    private Set<String> whitelist() {
        return Stream.of("/user/userAuth/register", "/user/userAuth/login", "/user/apiIdempotent/get/token",
                        "/order/hello", "/user/testCustomLimitFilter","/order/feignTest/testLogin"
                        ,"/order/feignTest/commonTest","/order/seataOrder/create")
                .collect(Collectors.toSet());
    }
}
